#include "Application.h"

#include <glm/gtc/matrix_transform.hpp>

#include <SOIL.h>

#include <iostream>
#include <fstream>
#include <cmath>
#include <string>

const GLfloat Application::QUAD_VERTEX_BUFFER[24] = 
{
	-1.0f,  1.0f,  0.0f, 1.0f,
	 1.0f,  1.0f,  1.0f, 1.0f,
	 1.0f, -1.0f,  1.0f, 0.0f,

	 1.0f, -1.0f,  1.0f, 0.0f,
	-1.0f, -1.0f,  0.0f, 0.0f,
	-1.0f,  1.0f,  0.0f, 1.0f
};

Application::Application() :
m_running(true),
m_camera(vec3(0.0f, 0.0f, 0.0f)),
m_quadVao(0),
m_quadVbo(0),
m_postprocFramebuffer(0),
m_postprocTexture(0),
m_postprocDepthRenderbuffer(0),
m_postprocTexLocation(0),
m_time(0.0)
{
}

Application::~Application()
{
    for (unsigned int i = 0; i < m_objects.size(); ++i)
    {
        delete m_objects[i];
    }

    for (unsigned int i = 0; i < m_shaders.size(); ++i)
    {
        delete m_shaders[i];
    }

    for (unsigned int i = 0; i < m_models.size(); ++i)
    {
        delete m_models[i];
    }

    for (unsigned int i = 0; i < m_materials.size(); ++i)
    {
        delete m_materials[i];
    }

    glDeleteFramebuffers(1, &m_postprocFramebuffer);
    glDeleteFramebuffers(1, &m_glowFramebuffer);
    glDeleteTextures(1, &m_postprocTexture);
    glDeleteTextures(1, &m_depthTexture);
    glDeleteTextures(1, &m_glowTexture);
    glDeleteVertexArrays(1, &m_quadVao);
    glDeleteBuffers(1, &m_quadVbo);
    glDeleteRenderbuffers(1, &m_postprocDepthRenderbuffer);
    glDeleteRenderbuffers(1, &m_glowDepthRenderbuffer);

    glfwTerminate();
}

void Application::initialize()
{    
    // GLFW.
    glfwInit();

    glfwOpenWindowHint(GLFW_FSAA_SAMPLES, 4);
    glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
    glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 3);
    glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwOpenWindowHint(GLFW_WINDOW_NO_RESIZE, GL_TRUE);

    glfwOpenWindow(WIDTH, HEIGHT, 0, 0, 0, 0, 24, 8, GLFW_WINDOW);
    glfwSetWindowPos(1000, 200);
    glfwSetWindowTitle("OpenGL");

    glfwSetMousePos(WIDTH / 2, HEIGHT / 2);
    m_mouse.setPosition(WIDTH / 2, HEIGHT / 2);

    glfwDisable(GLFW_MOUSE_CURSOR);
 
    // GLEW.
    glewExperimental = GL_TRUE;
    GLenum err = glewInit();

    if (GLEW_OK != err)
    {
        m_running = false;
        throw glewGetErrorString(err);
    }

    // GL.
    glDisable(GL_CULL_FACE);
    glEnable(GL_DEPTH_TEST);

    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);   

    glClearColor(0.0f, 0.0f, 0.2f, 1.0f);

    // Other.
    initPostprocess();
    initEntities();

    Object::projMatrix = glm::perspective(FOV, (float)WIDTH / (float)HEIGHT, NCP, FCP);
}

bool Application::running()
{
    if (!glfwGetWindowParam(GLFW_OPENED))
    {
        m_running = false;
    }

    return m_running;
}

void Application::handleInput()
{
    if (glfwGetKey(GLFW_KEY_ESC) == GLFW_PRESS)
    {
        m_running = false;
    }

    if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS)
    {
    }
    else if (glfwGetMouseButton(GLFW_MOUSE_BUTTON_LEFT) == GLFW_RELEASE)
    {        
    }

    // Movement.
    if (glfwGetKey('A'))
    {
        m_camera.strafeLeft();
    }

    if (glfwGetKey('D'))
    {
        m_camera.strafeRight();
    }

    if (glfwGetKey('W'))
    {
        m_camera.moveForward();
    }

    if (glfwGetKey('S'))
    {
        m_camera.moveBackwards();
    }

    if (glfwGetKey('A') || glfwGetKey('D'))
    {
        m_camera.setStrafeStatus(true);
    }
    else
    {
        m_camera.setStrafeStatus(false);
    }

    if (glfwGetKey('W') || glfwGetKey('S'))
    {
        m_camera.setMovementStatus(true);
    }
    else
    {
        m_camera.setMovementStatus(false);
    }

    // Reset.
    if (glfwGetKey('R'))
    {
        m_camera.setPosition(vec3(0.0f, 0.0f, 0.0f));
    }

    // Post-process..
    if (glfwGetKey('1'))
    {
        loadShader("post_none");
    }
    else if (glfwGetKey('2'))
    {
        loadShader("post_negative");
    }
    else if (glfwGetKey('3'))
    {
        loadShader("post_blur");
    }
    else if (glfwGetKey('4'))
    {
        loadShader("post_depth");
    }
    else if (glfwGetKey('5'))
    {
        loadShader("post_dof");
    }

    // Mouse.
    int x = 0;
    int y = 0;
    glfwGetMousePos(&x, &y);
    m_mouse.setPosition(x, y);
    m_camera.turn(vec2(m_mouse.getDeltaX(), m_mouse.getDeltaY()));
}

void Application::update()
{
    double elapsed = (glfwGetTime() - m_time) * 1000.0;
    m_time = glfwGetTime();

    vec3 newPos = m_lights[0].getPos();
    newPos.x = 2.0f * static_cast<float>(sin(glfwGetTime()));
    newPos.z = 2.0f * static_cast<float>(cos(glfwGetTime()));
    m_lights[0].setPos(newPos);

    Object::viewMatrix = glm::lookAt(m_camera.getPos(), m_camera.getLookAt(), CAMERA_UP);
    Object::lights = m_lights;    
    Object::gameTime = m_time;

    for (unsigned int i = 0; i < m_objects.size(); ++i)
    {
        if (m_objects[i]->isStateOk())
        {
            m_objects[i]->update(elapsed);
        }        
    }
}

void Application::render()
{
    // Glowmap.
    glBindFramebuffer(GL_FRAMEBUFFER, m_glowFramebuffer);
    glEnable(GL_DEPTH_TEST);    
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glColorMask(false, false, false, false);
    glViewport(0, 0, GLOWMAP_WIDTH, GLOWMAP_HEIGHT);

    for (unsigned int i = 0; i < m_objects.size(); ++i)
    {
        if (m_objects[i]->isStateOk() && !m_objects[i]->isEmissive())
        {
            m_objects[i]->render();
        }        
    }

    glColorMask(true, true, true, true);

    for (unsigned int i = 0; i < m_objects.size(); ++i)
    {
        if (m_objects[i]->isStateOk() && m_objects[i]->isEmissive())
        {
            m_objects[i]->render();
        }        
    }

    // Actual rendering.
    glBindFramebuffer(GL_FRAMEBUFFER, m_postprocFramebuffer);
    GLenum windowBuffClear[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
    glDrawBuffers(2, windowBuffClear);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
    glViewport(0, 0, WIDTH, HEIGHT);
    
    for (unsigned int i = 0; i < m_objects.size(); ++i)
    {
        if (m_objects[i]->isStateOk())
        {
            m_objects[i]->render();
        }        
    }

    // Post-process.
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glBindVertexArray(m_quadVao);
    glDisable(GL_DEPTH_TEST);
    glUseProgram(m_postprocess.id());

    GLuint location;

    location = glGetUniformLocation(m_postprocess.id(), "focalLength");
    glUniform1f(location, m_camera.getFocalLength());

    location = glGetUniformLocation(m_postprocess.id(), "focusDistance");
    glUniform1f(location, m_camera.getFocusDistance());

    location = glGetUniformLocation(m_postprocess.id(), "focalRatio");
    glUniform1f(location, m_camera.getFocalRatio());

    location = glGetUniformLocation(m_postprocess.id(), "PPM");
    glUniform1f(location, static_cast<float>(WIDTH) / m_camera.getFrameSize());

    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_postprocTexture);
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_2D, m_glowTexture);
    glActiveTexture(GL_TEXTURE2);
    glBindTexture(GL_TEXTURE_2D, m_depthTexture);

    glDrawArrays(GL_TRIANGLES, 0, 6);

    glfwSwapBuffers();
}

void Application::initEntities()
{
    std::ifstream entitiesFile("entities.dat");

    if (!entitiesFile.is_open())
    {
        std::cerr << "Unable to open file entities.dat." << std::endl;
        return;
    }
    
    string entityType = "";
    string trash = "";
    string model = "";
    string material = "";
    string shader = "";
    vec3 temp;

    while (entitiesFile >> entityType && entitiesFile.good())
    {
        // Lights.
        if (entityType == "light")
        {
            if (m_lights.size() < MAX_LIGHTS)
            {                
                vec3 pos;
                vec3 color;
                float power;

                entitiesFile >> pos.x >> pos.y >> pos.z 
                             >> color.r >> color.g >> color.b 
                             >> power;

                Light light(pos, color, power);
                m_lights.push_back(light);
            }
            else
            {
                std::cerr << "Maximum number of lights exceeded, current maximum is: " << MAX_LIGHTS << std::endl;
                std::getline(entitiesFile, trash);
            }
        }
        // Renderable objects.
        else if (entityType == "object")
        {
            entitiesFile >> model >> shader >> material;

            Shader* newShader = 0;
            Model* newModel = 0;
            Material* newMaterial = 0;

            SetObjectReference<vector<Shader*>, Shader*, Shader>(m_shaders, newShader, shader);
            SetObjectReference<vector<Model*>, Model*, Model>(m_models, newModel, model);
            SetObjectReference<vector<Material*>, Material*, Material>(m_materials, newMaterial, material);

            Object* obj = new Object(newShader, newModel, newMaterial);
            
            entitiesFile >> temp.x >> temp.y >> temp.z;
            obj->setPosition(temp);

            entitiesFile >> temp.x >> temp.y >> temp.z;
            obj->setOrientation(temp);

            entitiesFile >> temp.x >> temp.y >> temp.z;
            obj->setMovement(temp);

            entitiesFile >> temp.x >> temp.y >> temp.z;
            obj->setRotation(temp);

            m_objects.push_back(obj);    
        }
        // Comment.
        else if (entityType == "//")
        {
            std::getline(entitiesFile, trash);
        }
        // Error.
        else if (entityType == "camera")
        {
            entitiesFile >> temp.x >> temp.y >> temp.z;
            m_camera.setPosition(temp);
        }
        else
        {
            std::cerr << "Error while reading file entities.dat" << std::endl;
        }
    }

    Object::lights = m_lights;
}

void Application::initPostprocess()
{
    // Post-process quad and shader.
    glGenVertexArrays(1, &m_quadVao);
    glGenBuffers(1, &m_quadVbo);

    glBindBuffer(GL_ARRAY_BUFFER, m_quadVbo);
    glBufferData(GL_ARRAY_BUFFER, sizeof(QUAD_VERTEX_BUFFER), QUAD_VERTEX_BUFFER, GL_STATIC_DRAW);

    if (!m_postprocess.init("post_none"))
    {
        std::cerr << "Shader initialization failed." << std::endl;
    }

    glBindVertexArray(m_quadVao);
    glBindBuffer(GL_ARRAY_BUFFER, m_quadVbo);

    GLint posAttrib = glGetAttribLocation(m_postprocess.id(), "position");
    glEnableVertexAttribArray(posAttrib);
    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), 0);

    GLint texAttrib = glGetAttribLocation(m_postprocess.id(), "texcoord");
    glEnableVertexAttribArray(texAttrib);
    glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));

    // Post-process and glow textures.
    glUseProgram(m_postprocess.id());

    m_postprocTexLocation = glGetUniformLocation(m_postprocess.id(), "postprocTexture");
    glUniform1i(m_postprocTexLocation, 0);
    m_glowTexLocation  = glGetUniformLocation(m_postprocess.id(), "glowmapTexture");
    glUniform1i(m_glowTexLocation, 1);
    m_depthTexLocation  = glGetUniformLocation(m_postprocess.id(), "depthTexture");
    glUniform1i(m_depthTexLocation, 2);

    // Post-process
    glGenFramebuffers(1, &m_postprocFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, m_postprocFramebuffer);

    glGenTextures(1, &m_postprocTexture);
    glBindTexture(GL_TEXTURE_2D, m_postprocTexture);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glGenTextures(1, &m_depthTexture);
    glBindTexture(GL_TEXTURE_2D, m_depthTexture);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, WIDTH, HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_postprocTexture, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, m_depthTexture, 0);

    glGenRenderbuffers(1, &m_postprocDepthRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, m_postprocDepthRenderbuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, WIDTH, HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_postprocDepthRenderbuffer);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
        std::cerr << "Framebuffer status is not complete." << std::endl;
    }

    // Glow map.
    glGenFramebuffers(1, &m_glowFramebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, m_glowFramebuffer);

    glGenTextures(1, &m_glowTexture);
    glBindTexture(GL_TEXTURE_2D, m_glowTexture);

    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, GLOWMAP_WIDTH, GLOWMAP_HEIGHT, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); 
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_glowTexture, 0);

    glGenRenderbuffers(1, &m_glowDepthRenderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, m_glowDepthRenderbuffer);
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, GLOWMAP_WIDTH, GLOWMAP_HEIGHT);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, m_glowDepthRenderbuffer);

    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
        std::cerr << "Framebuffer status is not complete." << std::endl;
    }
}

void Application::setTextureUniforms()
{
    m_postprocTexLocation = glGetUniformLocation(m_postprocess.id(), "postprocTexture");
    glUniform1i(m_postprocTexLocation, 0);
    m_glowTexLocation  = glGetUniformLocation(m_postprocess.id(), "glowmapTexture");
    glUniform1i(m_glowTexLocation, 1);
    m_depthTexLocation  = glGetUniformLocation(m_postprocess.id(), "depthTexture");
    glUniform1i(m_depthTexLocation, 2);
}

void Application::loadShader(const string& shader)
{
    if (!m_postprocess.loadShaders(shader))
    {
        std::cerr << "Shader initialization failed." << std::endl;
    }

    setTextureUniforms();
}

template <class T, class U, class V> 
void Application::SetObjectReference(T& container, U& reference, const string& name)
{
    for (unsigned int i = 0; i < container.size(); ++i)
    {
        if (container[i]->getName() == name)
        {
            reference = container[i];
            break;                    
        }
    }

    if (reference == 0)
    {
        reference = new V(name);
        container.push_back(reference);
    }
}
